package de.dhbw_stuttgart.wi2011i.outofapples.persistence;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Scanner;
import java.util.UUID;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.simpleframework.xml.Serializer;

import android.content.Context;
import android.content.res.Resources.NotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.Session;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.ShoppingItem;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.ShoppingList;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.UserDTO;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.collections.ShoppingItemCollection;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.collections.ShoppingListCollection;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.collections.UserCollection;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.links.ShoppingItemLink;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.links.ShoppingListLink;
import de.dhbw_stuttgart.wi2011i.outofapples.dtos.links.UserLink;
import de.dhbw_stuttgart.wi2011i.outofapples.logging.Logger;
import de.dhbw_stuttgart.wi2011i.outofapples.persistence.exceptions.ConflictException;
import de.dhbw_stuttgart.wi2011i.outofapples.persistence.exceptions.EinkaufsblockException;
import de.dhbw_stuttgart.wi2011i.outofapples.persistence.exceptions.NetworkException;
import de.dhbw_stuttgart.wi2011i.outofapples.server.parameters.LoginParameter;
import de.dhbw_stuttgart.wi2011i.outofapples.server.parameters.ShoppingItemParameter;
import de.dhbw_stuttgart.wi2011i.outofapples.server.parameters.ShoppingListParameter;

public final class OnlineEinkaufsblock extends EinkaufsblockBase implements Einkaufsblock {
	private final Serializer serializer;
	private final DefaultHttpClient client;
	private final Context context;
	private String url;
	
	public OnlineEinkaufsblock(Logger logger, Serializer serializer, Context context, DefaultHttpClient httpClient) {
		super(logger);
		
		HttpParams httpParameters = httpClient.getParams();
		HttpConnectionParams.setConnectionTimeout(httpParameters, 1000);
		HttpConnectionParams.setSoTimeout(httpParameters, 1000);
		
		this.serializer = serializer;
		this.context = context;
		this.client = httpClient;
	}
		
	public Collection<UserDTO> getUsers() {
		ArrayList<UserDTO> result = new ArrayList<UserDTO>();
		
		for (UserLink link : getNotNull(UserCollection.class, "user").getElements()) {
			UserDTO user = getUserByName(link.getName());
			result.add(user);
		}
		
		return result;
	}
	public UserDTO getUserByName(String username) {
		return getNotNull(UserDTO.class, "user/%s", username);
	}

	public UserDTO getCurrentUser() {
		UserLink link = this.getSession().getUser();
		
		return this.getUserByName(link.getName());
	}

	public Collection<ShoppingList> getLists() {
		ArrayList<ShoppingList> result = new ArrayList<ShoppingList>();
		
		for (ShoppingListLink link : this.getNotNull(ShoppingListCollection.class, "list").getElements()) {
			ShoppingList list = this.getListById(link.getId());
			result.add(list);
		}
		
		return result;
	}

	public ShoppingList getListById(UUID id) {
		return this.getNotNull(ShoppingList.class, "list/%s", id);
	}

	public void addList(ShoppingList list) {
		ShoppingListParameter parameter = new ShoppingListParameter(list);
		this.put(parameter, "list/%s", list.getId());
		
		this.triggerBlockChange();
	}

	public void delete(ShoppingList list) {
		this.delete("list/%s", list.getId());
		
		this.triggerBlockChange();
	}

	public void update(ShoppingList list) {
		ShoppingListParameter parameter = new ShoppingListParameter(list);
		this.put(parameter, "list/%s", list.getId());

		this.triggerListChange(list.getId());
	}

	public Collection<ShoppingItem> getItems(ShoppingList list) {
		ArrayList<ShoppingItem> result = new ArrayList<ShoppingItem>();
		
		for (ShoppingItemLink link : this.getNotNull(ShoppingItemCollection.class, "list/%s/item", list.getId()).getElements()) {
			ShoppingItem item = this.getItemById(link.getId());
			result.add(item);
		}
		
		return result;
	}

	public ShoppingItem getItemById(UUID id) {
		return this.getNotNull(ShoppingItem.class, "item/%s", id);
	}

	public void addItem(UUID parentId, ShoppingItem item) {
		ShoppingItemParameter parameter = new ShoppingItemParameter(item);
		this.put(parameter, "list/%s/item/%s", parentId, item.getId());
		
		this.triggerListChange(parentId);
	}

	public void update(ShoppingItem item) {
		ShoppingItemParameter parameter = new ShoppingItemParameter(item);
		this.put(parameter, "item/%s", item.getId());
		
		this.triggerItemChange(item.getId());
	}

	public void deleteItem(ShoppingItem item) {
		this.delete("item/%s", item.getId());
		
		this.triggerListChange(item.getParentId());
	}

	public void shareList(ShoppingList liste, String username) {
		// TODO Auto-generated method stub
		
	}
	
	public String getSessionCookie() {		
		if (this.client.getCookieStore().getCookies().size() == 0)
			return null;
		
		return this.client.getCookieStore().getCookies().get(0).getValue();
	}
	public void setSessionCookie(String sessionCookie) {
		this.logger.info("Setting sessionId: %s", sessionCookie);
		
		this.client.getCookieStore().clear();
		
		BasicClientCookie cookie = new BasicClientCookie("JSESSIONID", sessionCookie);
		cookie.setPath("/");
		cookie.setDomain(Uri.parse(this.getUrl()).getHost());
		this.client.getCookieStore().addCookie(cookie);
	}

	public boolean isConnected() {
		if (this.url == null)
			return false;
		
		ConnectivityManager cm = (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE);
	    NetworkInfo netInfo = cm.getActiveNetworkInfo();
		return netInfo != null && netInfo.isConnectedOrConnecting();		    
	}

	public boolean isLoggedIn() {
		return this.getSession() != null;
	}

	public boolean tryLogin(String username, String password) {
		this.logger.info("Trying to log in as %s", username);
		
		LoginParameter loginParameter = new LoginParameter();
		loginParameter.setUsername(username);
		loginParameter.setPassword(password);
		
		this.post(loginParameter, "session");
		
		return true;
	}

	public void logout() {
		this.logger.info("Logging out");
		
		this.delete("session");
	}

	private Session getSession() {
		this.logger.info("Getting session");
		
		Session result = get(Session.class, "session");
		if (result == null)
			this.logger.info("Session is null");
		
		return result;
	}
	
	public String getUrl() {
		return this.url;
	}
	public void setUrl(String url) {
		this.url = url;
	}

	private <T> T getNotNull(Class<? extends T> clazz, String uri, Object... args) {
		T result = this.get(clazz, uri, args);
		if (result == null)
			throw new NotFoundException();
		
		return result;
	}
	private void delete(String uri, Object... args) {
		URI url = this.formatUrl(uri, args);
		
		HttpDelete delete = new HttpDelete(url);
		HttpResponse response = this.execute(delete);
		
		this.verifySuccess(response);
	}
	private <T> void post(T item, String uri, Object... args) {
		this.logger.info("A");
		URI url = this.formatUrl(uri, args);

		this.logger.info("B");
		HttpPost post = new HttpPost(url);
		this.logger.info("C");
		post.setHeader("Content-Type", "application/xml");
		this.logger.info("D");
		post.setEntity(this.serialize(item));

		this.logger.info("E");
		HttpResponse response = this.execute(post);
		this.logger.info("F");
		this.verifySuccess(response);

		this.logger.info("G");
	}
	private <T> void put(T item, String uri, Object... args) {
		URI url = this.formatUrl(uri, args);
		
		HttpPut put = new HttpPut(url);
		put.setHeader("Content-Type", "application/xml");
		put.setEntity(this.serialize(item));
		
		HttpResponse response = this.execute(put);
		this.verifySuccess(response);
	}
	private <T> T get(Class<? extends T> clazz, String uri, Object... args) {
		URI url = this.formatUrl(uri, args);
		
		HttpGet get = new HttpGet(url);
		HttpResponse response = this.execute(get);
		if (response.getStatusLine().getStatusCode() == 404)
			return null;
		
		this.verifySuccess(response);
		return this.deserialize(response, clazz);
	}
	private void verifySuccess(HttpResponse response) {
		int statusCode = response.getStatusLine().getStatusCode();
		this.logger.info("Statuscode: %s", statusCode);
		
		if (statusCode / 100 != 2) {
			this.logger.warning("Not successful %s", statusCode);

			try {
				String content = new Scanner(response.getEntity().getContent()).useDelimiter("\\A").next();
				this.logger.warning("Server returned:\n\n%s", content);
			}
			catch (Exception e) {
				this.logger.error("Error reading error output", e);
			}
			
			if (statusCode == 409)
				throw new ConflictException();
			
			throw new EinkaufsblockException("Status code: " + statusCode);
		}
	}
	
	private HttpResponse execute(HttpRequestBase request) {
		this.logger.info("%s: %s", request.getClass().getName(), request.getURI());
		
		try {
			return this.client.execute(request);
		} catch (ClientProtocolException e) {
			this.logger.error("Error requesting from " + url, e);
			
			throw new NetworkException("Network exception", e);
		} catch (IOException e) {
			this.logger.error("Error requesting from " + url, e);
			
			throw new NetworkException("Network exception", e);
		}
		
	}

	private URI formatUrl(String url, Object[] args) {
		String result = this.getUrl();
		if (!result.endsWith("/"))
			result += "/";
		result += "rest/";
		
		result += String.format(url, args);
		
		try {
			return new URI(result);
		} catch (URISyntaxException e) {
			this.logger.error("Error creating URI " + url, e);
			
			throw new RuntimeException(e);
		}
	}
	
	private <T> HttpEntity serialize(T entity) {
		try {
			StringWriter writer = new StringWriter();
			this.serializer.write(entity, writer);
			
			String xml = writer.toString();
			writer.close();
			
			return new StringEntity(xml);
		}
		catch (Exception ex) {
			this.logger.error("Error during serialization", ex);
			
			throw new RuntimeException(ex);
		}
	}
	private <T> T deserialize(HttpResponse response, Class<T> clazz) {
		InputStream stream = null;
		try {
			stream = response.getEntity().getContent();
			return this.serializer.read(clazz, stream);
		}
		catch (Exception ex) {
			this.logger.error("Error deserializing response", ex);
			
			throw new RuntimeException(ex);
		}
		finally {
			try {
				if (stream != null)
					stream.close();				
			}
			catch (IOException ex) {
				//Ignore exception on close
			}
		}
	}
}
